/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2008 Sun Microsystems, Inc.
 * Portions Copyright 2015-2016 ForgeRock AS.
 */

package org.forgerock.opendj.config;

import org.forgerock.util.Reject;

import java.util.Comparator;
import java.util.EnumSet;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.Set;

import org.forgerock.i18n.LocalizableMessage;

/**
 * An interface for querying generic property definition features.
 * <p>
 * Property definitions are analogous to ConfigAttributes in the current model
 * and will play a similar role. Eventually these will replace them.
 * <p>
 * Implementations <b>must</b> take care to implement the various comparison
 * methods.
 *
 * @param <T>
 *            The data-type of values of the property.
 */
public abstract class PropertyDefinition<T> implements Comparator<T>, Comparable<PropertyDefinition<?>> {

    /**
     * An interface for incrementally constructing property definitions.
     *
     * @param <T>
     *            The data-type of values of the property.
     * @param <D>
     *            The type of property definition constructed by this builder.
     */
    protected static abstract class AbstractBuilder<T, D extends PropertyDefinition<T>> {

        /** The administrator action. */
        private AdministratorAction adminAction;

        /** The default behavior provider. */
        private DefaultBehaviorProvider<T> defaultBehavior;

        /** The abstract managed object. */
        private final AbstractManagedObjectDefinition<?, ?> definition;

        /** The options applicable to this definition. */
        private final EnumSet<PropertyOption> options;

        /** The name of this property definition. */
        private final String propertyName;

        /**
         * Create a property definition builder.
         *
         * @param d
         *            The managed object definition associated with this
         *            property definition.
         * @param propertyName
         *            The property name.
         */
        protected AbstractBuilder(AbstractManagedObjectDefinition<?, ?> d, String propertyName) {
            this.definition = d;
            this.propertyName = propertyName;
            this.options = EnumSet.noneOf(PropertyOption.class);
            this.adminAction = new AdministratorAction(AdministratorAction.Type.NONE, d, propertyName);
            this.defaultBehavior = new UndefinedDefaultBehaviorProvider<>();
        }

        /**
         * Construct a property definition based on the properties of this
         * builder.
         *
         * @return The new property definition.
         */
        public final D getInstance() {
            return buildInstance(definition, propertyName, options, adminAction, defaultBehavior);
        }

        /**
         * Set the administrator action.
         *
         * @param adminAction
         *            The administrator action.
         */
        public final void setAdministratorAction(AdministratorAction adminAction) {
            Reject.ifNull(adminAction);
            this.adminAction = adminAction;
        }

        /**
         * Set the default behavior provider.
         *
         * @param defaultBehavior
         *            The default behavior provider.
         */
        public final void setDefaultBehaviorProvider(DefaultBehaviorProvider<T> defaultBehavior) {
            Reject.ifNull(defaultBehavior);
            this.defaultBehavior = defaultBehavior;
        }

        /**
         * Add a property definition option.
         *
         * @param option
         *            The property option.
         */
        public final void setOption(PropertyOption option) {
            Reject.ifNull(option);
            options.add(option);
        }

        /**
         * Build a property definition based on the properties of this builder.
         *
         * @param d
         *            The managed object definition associated with this
         *            property definition.
         * @param propertyName
         *            The property name.
         * @param options
         *            Options applicable to this definition.
         * @param adminAction
         *            The administrator action.
         * @param defaultBehavior
         *            The default behavior provider.
         * @return The new property definition.
         */
        protected abstract D buildInstance(AbstractManagedObjectDefinition<?, ?> d, String propertyName,
            EnumSet<PropertyOption> options, AdministratorAction adminAction,
            DefaultBehaviorProvider<T> defaultBehavior);
    }

    /** The administrator action. */
    private final AdministratorAction adminAction;

    /** The default behavior provider. */
    private final DefaultBehaviorProvider<T> defaultBehavior;

    /** The abstract managed object. */
    private final AbstractManagedObjectDefinition<?, ?> definition;

    /** Options applicable to this definition. */
    private final Set<PropertyOption> options;

    /** The property name. */
    private final String propertyName;

    /** The property value class. */
    private final Class<T> theClass;

    /**
     * Create a property definition.
     *
     * @param d
     *            The managed object definition associated with this property
     *            definition.
     * @param theClass
     *            The property value class.
     * @param propertyName
     *            The property name.
     * @param options
     *            Options applicable to this definition.
     * @param adminAction
     *            The administrator action.
     * @param defaultBehavior
     *            The default behavior provider.
     */
    protected PropertyDefinition(AbstractManagedObjectDefinition<?, ?> d, Class<T> theClass, String propertyName,
        EnumSet<PropertyOption> options, AdministratorAction adminAction, DefaultBehaviorProvider<T> defaultBehavior) {
        Reject.ifNull(d, theClass, propertyName, options, adminAction, defaultBehavior);

        this.definition = d;
        this.theClass = theClass;
        this.propertyName = propertyName;
        this.options = EnumSet.copyOf(options);
        this.adminAction = adminAction;
        this.defaultBehavior = defaultBehavior;
    }

    /**
     * Apply a visitor to this property definition.
     *
     * @param <R>
     *            The return type of the visitor's methods.
     * @param <P>
     *            The type of the additional parameters to the visitor's
     *            methods.
     * @param v
     *            The property definition visitor.
     * @param p
     *            Optional additional visitor parameter.
     * @return Returns a result as specified by the visitor.
     */
    public abstract <R, P> R accept(PropertyDefinitionVisitor<R, P> v, P p);

    /**
     * Apply a visitor to a property value associated with this property
     * definition.
     *
     * @param <R>
     *            The return type of the visitor's methods.
     * @param <P>
     *            The type of the additional parameters to the visitor's
     *            methods.
     * @param v
     *            The property value visitor.
     * @param value
     *            The property value.
     * @param p
     *            Optional additional visitor parameter.
     * @return Returns a result as specified by the visitor.
     */
    public abstract <R, P> R accept(PropertyValueVisitor<R, P> v, T value, P p);

    /**
     * Cast the provided value to the type associated with this property
     * definition.
     * <p>
     * This method only casts the object to the required type; it does not
     * validate the value once it has been cast. Subsequent validation should be
     * performed using the method {@link #validateValue(Object)}.
     * <p>
     * This method guarantees the following expression is always
     * <code>true</code>:
     *
     * <pre>
     *  PropertyDefinition d;
     *  x == d.cast(x);
     * </pre>
     *
     * @param object
     *            The property value to be cast (can be <code>null</code>).
     * @return Returns the property value cast to the correct type.
     * @throws ClassCastException
     *             If the provided property value did not have the correct type.
     */
    public final T castValue(Object object) {
        return theClass.cast(object);
    }

    /**
     * Compares two property values for order. Returns a negative integer, zero,
     * or a positive integer as the first argument is less than, equal to, or
     * greater than the second.
     * <p>
     * This default implementation normalizes both values using
     * {@link #normalizeValue(Object)} and then performs a case-sensitive string
     * comparison.
     *
     * @param o1
     *            the first object to be compared.
     * @param o2
     *            the second object to be compared.
     * @return a negative integer, zero, or a positive integer as the first
     *         argument is less than, equal to, or greater than the second.
     */
    @Override
    public int compare(T o1, T o2) {
        Reject.ifNull(o1);
        Reject.ifNull(o2);

        String s1 = normalizeValue(o1);
        String s2 = normalizeValue(o2);

        return s1.compareTo(s2);
    }

    /**
     * Compares this property definition with the specified property definition
     * for order. Returns a negative integer, zero, or a positive integer if
     * this property definition is less than, equal to, or greater than the
     * specified property definition.
     * <p>
     * The ordering must be determined first from the property name and then
     * base on the underlying value type.
     *
     * @param o
     *            The reference property definition with which to compare.
     * @return Returns a negative integer, zero, or a positive integer if this
     *         property definition is less than, equal to, or greater than the
     *         specified property definition.
     */
    @Override
    public final int compareTo(PropertyDefinition<?> o) {
        int rc = propertyName.compareTo(o.propertyName);
        if (rc == 0) {
            rc = theClass.getName().compareTo(o.theClass.getName());
        }
        return rc;
    }

    /**
     * Parse and validate a string representation of a property value.
     *
     * @param value
     *            The property string value (must not be <code>null</code>).
     * @return Returns the decoded property value.
     * @throws PropertyException
     *             If the property value string is invalid.
     */
    public abstract T decodeValue(String value);

    /**
     * Encode the provided property value into its string representation.
     * <p>
     * This default implementation simply returns invokes the
     * {@link Object#toString()} method on the provided value.
     *
     * @param value
     *            The property value (must not be <code>null</code>).
     * @return Returns the encoded property string value.
     * @throws PropertyException
     *             If the property value is invalid.
     */
    public String encodeValue(T value) {
        Reject.ifNull(value);

        return value.toString();
    }

    /**
     * Indicates whether some other object is &quot;equal to&quot; this property
     * definition. This method must obey the general contract of
     * <tt>Object.equals(Object)</tt>. Additionally, this method can return
     * <tt>true</tt> <i>only</i> if the specified Object is also a property
     * definition and it has the same name, as returned by {@link #getName()},
     * and also is deemed to be &quot;compatible&quot; with this property
     * definition. Compatibility means that the two property definitions share
     * the same underlying value type and provide similar comparator
     * implementations.
     *
     * @param o
     *            The reference object with which to compare.
     * @return Returns <code>true</code> only if the specified object is also a
     *         property definition and it has the same name and is compatible
     *         with this property definition.
     * @see java.lang.Object#equals(java.lang.Object)
     * @see java.lang.Object#hashCode()
     */
    @Override
    public final boolean equals(Object o) {
        if (this == o) {
            return true;
        } else if (o instanceof PropertyDefinition) {
            PropertyDefinition<?> other = (PropertyDefinition<?>) o;
            return propertyName.equals(other.propertyName)
                    && theClass.equals(other.theClass);
        } else {
            return false;
        }
    }

    /**
     * Get the administrator action associated with this property definition.
     * The administrator action describes any action which the administrator
     * must perform in order for changes to this property to take effect.
     *
     * @return Returns the administrator action associated with this property
     *         definition.
     */
    public final AdministratorAction getAdministratorAction() {
        return adminAction;
    }

    /**
     * Get the default behavior provider associated with this property
     * definition.
     *
     * @return Returns the default behavior provider associated with this
     *         property definition.
     */
    public final DefaultBehaviorProvider<T> getDefaultBehaviorProvider() {
        return defaultBehavior;
    }

    /**
     * Gets the optional description of this property definition in the default
     * locale.
     *
     * @return Returns the description of this property definition in the
     *         default locale, or <code>null</code> if there is no description.
     */
    public final LocalizableMessage getDescription() {
        return getDescription(Locale.getDefault());
    }

    /**
     * Gets the optional description of this property definition in the
     * specified locale.
     *
     * @param locale
     *            The locale.
     * @return Returns the description of this property definition in the
     *         specified locale, or <code>null</code> if there is no
     *         description.
     */
    public final LocalizableMessage getDescription(Locale locale) {
        ManagedObjectDefinitionI18NResource resource = ManagedObjectDefinitionI18NResource.getInstance();
        String property = "property." + propertyName + ".description";
        try {
            return resource.getMessage(definition, property, locale);
        } catch (MissingResourceException e) {
            return null;
        }
    }

    /**
     * Gets the managed object definition associated with this property
     * definition.
     *
     * @return Returns the managed object definition associated with this
     *         property definition.
     */
    public final AbstractManagedObjectDefinition<?, ?> getManagedObjectDefinition() {
        return definition;
    }

    /**
     * Get the name of the property.
     *
     * @return Returns the name of the property.
     */
    public final String getName() {
        return propertyName;
    }

    /**
     * Gets the synopsis of this property definition in the default locale.
     *
     * @return Returns the synopsis of this property definition in the default
     *         locale.
     */
    public final LocalizableMessage getSynopsis() {
        return getSynopsis(Locale.getDefault());
    }

    /**
     * Gets the synopsis of this property definition in the specified locale.
     *
     * @param locale
     *            The locale.
     * @return Returns the synopsis of this property definition in the specified
     *         locale.
     */
    public final LocalizableMessage getSynopsis(Locale locale) {
        ManagedObjectDefinitionI18NResource resource = ManagedObjectDefinitionI18NResource.getInstance();
        String property = "property." + propertyName + ".synopsis";
        return resource.getMessage(definition, property, locale);
    }

    /**
     * Returns a hash code value for this property definition. The hash code
     * should be derived from the property name and the type of values handled
     * by this property definition.
     *
     * @return Returns the hash code value for this property definition.
     */
    @Override
    public final int hashCode() {
        int rc = 17 + propertyName.hashCode();
        return 37 * rc + theClass.hashCode();
    }

    /**
     * Check if the specified option is set for this property definition.
     *
     * @param option
     *            The option to test.
     * @return Returns <code>true</code> if the option is set, or
     *         <code>false</code> otherwise.
     */
    public final boolean hasOption(PropertyOption option) {
        return options.contains(option);
    }

    /**
     * Get a normalized string representation of a property value. This can then
     * be used for comparisons and for generating hash-codes.
     * <p>
     * This method may throw an exception if the provided value is invalid.
     * However, applications should not assume that implementations of this
     * method will always validate a value. This task is the responsibility of
     * {@link #validateValue(Object)}.
     * <p>
     * This default implementation simply returns the string representation of
     * the provided value. Sub-classes might want to override this method if
     * this behavior is insufficient (for example, a string property definition
     * might strip white-space and convert characters to lower-case).
     *
     * @param value
     *            The property value to be normalized.
     * @return Returns the normalized property value.
     * @throws PropertyException
     *             If the property value is invalid.
     */
    public String normalizeValue(T value) {
        Reject.ifNull(value);

        return encodeValue(value);
    }

    /**
     * Returns a string representation of this property definition.
     *
     * @return Returns a string representation of this property definition.
     * @see Object#toString()
     */
    @Override
    public final String toString() {
        StringBuilder builder = new StringBuilder();
        toString(builder);
        return builder.toString();
    }

    /**
     * Append a string representation of the property definition to the provided
     * string builder.
     * <p>
     * This simple implementation just outputs the propertyName of the property
     * definition. Sub-classes should override this method to provide more
     * complete string representations.
     *
     * @param builder
     *            The string builder where the string representation should be
     *            appended.
     */
    public void toString(StringBuilder builder) {
        builder.append(propertyName);
    }

    /**
     * Determine if the provided property value is valid according to this
     * property definition.
     *
     * @param value
     *            The property value (must not be <code>null</code>).
     * @throws PropertyException
     *             If the property value is invalid.
     */
    public abstract void validateValue(T value);

    /**
     * Performs any run-time initialization required by this property
     * definition. This may include resolving managed object paths and property
     * names.
     *
     * @throws Exception
     *             If this property definition could not be initialized.
     */
    protected void initialize() throws Exception {
        // No implementation required.
    }
}
